/* Copyright Statement: * * This software/firmware and related documentation ("MediaTek Software") are * protected under relevant copyright laws. The information contained herein * is confidential and proprietary to MediaTek Inc. and/or its licensors. * Without the prior written permission of MediaTek inc. and/or its licensors, * any reproduction, modification, use or disclosure of MediaTek Software, * and information contained herein, in whole or in part, shall be strictly prohibited. */ /* MediaTek Inc. (C) 2010. All rights reserved. * * BY OPENING THIS FILE, RECEIVER HEREBY UNEQUIVOCALLY ACKNOWLEDGES AND AGREES * THAT THE SOFTWARE/FIRMWARE AND ITS DOCUMENTATIONS ("MEDIATEK SOFTWARE") * RECEIVED FROM MEDIATEK AND/OR ITS REPRESENTATIVES ARE PROVIDED TO RECEIVER ON * AN "AS-IS" BASIS ONLY. MEDIATEK EXPRESSLY DISCLAIMS ANY AND ALL WARRANTIES, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NONINFRINGEMENT. * NEITHER DOES MEDIATEK PROVIDE ANY WARRANTY WHATSOEVER WITH RESPECT TO THE * SOFTWARE OF ANY THIRD PARTY WHICH MAY BE USED BY, INCORPORATED IN, OR * SUPPLIED WITH THE MEDIATEK SOFTWARE, AND RECEIVER AGREES TO LOOK ONLY TO SUCH * THIRD PARTY FOR ANY WARRANTY CLAIM RELATING THERETO. RECEIVER EXPRESSLY ACKNOWLEDGES * THAT IT IS RECEIVER'S SOLE RESPONSIBILITY TO OBTAIN FROM ANY THIRD PARTY ALL PROPER LICENSES * CONTAINED IN MEDIATEK SOFTWARE. MEDIATEK SHALL ALSO NOT BE RESPONSIBLE FOR ANY MEDIATEK * SOFTWARE RELEASES MADE TO RECEIVER'S SPECIFICATION OR TO CONFORM TO A PARTICULAR * STANDARD OR OPEN FORUM. RECEIVER'S SOLE AND EXCLUSIVE REMEDY AND MEDIATEK'S ENTIRE AND * CUMULATIVE LIABILITY WITH RESPECT TO THE MEDIATEK SOFTWARE RELEASED HEREUNDER WILL BE, * AT MEDIATEK'S OPTION, TO REVISE OR REPLACE THE MEDIATEK SOFTWARE AT ISSUE, * OR REFUND ANY SOFTWARE LICENSE FEES OR SERVICE CHARGE PAID BY RECEIVER TO * MEDIATEK FOR SUCH MEDIATEK SOFTWARE AT ISSUE. * * The following software/firmware and/or related documentation ("MediaTek Software") * have been modified by MediaTek Inc. All revisions are subject to any receiver's * applicable license agreements with MediaTek Inc. */ package com.android.music; import java.lang.Integer; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.Collections; import java.util.Vector; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Locale; import java.util.regex.Pattern; import com.android.music.R; import android.content.Context; import android.util.Log; class LyricsBody { /* * static members. */ public static final int TAG_UNKNOWN = -1; public static final int TAG_TRACK_INFO_AL = 0; public static final int TAG_TRACK_INFO_AR = 1; public static final int TAG_TRACK_INFO_BY = 2; public static final int TAG_TRACK_INFO_RE = 3; public static final int TAG_TRACK_INFO_TI = 4; public static final int TAG_TRACK_INFO_VE = 5; public static final int INTERVAL_INVALID = -1; public static final int INTERVAL_LAST_SENTENCE = -1; public static final int INVALID_INDEX = -1; private static final String LOG_TAG = "LyricsBody"; private static String mLrcErrorMsg = null; // if some error happens with the lyrics file // the error information is stored here. /* * data members. */ private LyricSentence mAlbumName = null; private LyricSentence mArtistName = null; private LyricSentence mTrackTitle = null; private LyricSentence mLyricAuthor = null; private LyricSentence mLyricBuilder = null; private LyricSentence mLyricBuilderVer = null; private int mAdjustMilliSec = 0; // this is the actual lyric content private Vector<LyricSentence> mLyricContent = new Vector<LyricSentence>(); // for index search private int mLastIndex = INVALID_INDEX; private int mLastMillisec = 0; // the application context private Context mContext = null; /* * constructor */ private LyricsBody(Context context) { mContext = context; String str = context.getResources().getString(R.string.lrc_unknown_info_tag); mAlbumName = new LyricSentence(str); mArtistName = new LyricSentence(str); mTrackTitle = new LyricSentence(str); mLyricAuthor = new LyricSentence(str); mLyricBuilder = new LyricSentence(str); mLyricBuilderVer = new LyricSentence(str); } /* * parse the lyrics from a .lrc/.LRC file. * * [in]context: application context * [in]filepathname: the absolute path-name of the .lrc/.LRC file * * return value: a LyricsBody object. null if this method fails; call "getLastLrcErrorMsg()" to * retrieve the failure message. */ public static LyricsBody getLyric(Context context, String filepathname) { if (null == filepathname || null == context) { throw new IllegalArgumentException(); } LyricsBody lyric = new LyricsBody(context); try { lyric.parseLrc(filepathname); } catch (LrcFileNotFoundException e) { mLrcErrorMsg = new String(e.getMessage()); Log.d(LOG_TAG, "getLyric : .lrc file not found."); return null; } catch (LrcFileUnsupportedEncodingException e) { mLrcErrorMsg = new String(e.getMessage()); Log.d(LOG_TAG, "getLyric : unsupported textual encoding."); return null; } catch (LrcFileIOException e) { mLrcErrorMsg = new String(e.getMessage()); Log.d(LOG_TAG, "getLyric : I/O error when accessing .lrc file."); return null; } catch (LrcFileInvalidFormatException e) { mLrcErrorMsg = new String(e.getMessage()); Log.d(LOG_TAG, "getLyric : Invalid LRC file format."); return null; } return lyric; // if success } /* * returns the error message of last call to "getLyric" */ public static String getLastLrcErrorMsg() { String str = new String(mLrcErrorMsg); return str; } /* * get the information of the lyrics. */ public String getLyricInfo(int infoTag) { switch (infoTag) { case TAG_TRACK_INFO_AL: return mAlbumName.getSentence(); case TAG_TRACK_INFO_AR: return mArtistName.getSentence(); case TAG_TRACK_INFO_BY: return mLyricAuthor.getSentence(); case TAG_TRACK_INFO_RE: return mLyricBuilder.getSentence(); case TAG_TRACK_INFO_TI: return mTrackTitle.getSentence(); case TAG_TRACK_INFO_VE: return mLyricBuilderVer.getSentence(); default: Log.d(LOG_TAG, "getLyricInfo : invalid {inforTag} parameter."); throw new IllegalArgumentException(); } } public int getLyricOffset() { return mAdjustMilliSec; } public int getSentenceCnt() { return mLyricContent.size(); } public String getSentenceText(int index) { return mLyricContent.elementAt(index).getSentence(); } public int getSentenceTime(int index) { return mLyricContent.elementAt(index).getTime(); } /* * get the time interval (in millisecond) between the current sentence and the next one. * * [in]index: the index of current sentence. * * return value: the time interval. -1 if the interval is not valid. */ public int getIntervalToNext(int index) { if (0 <= index && index <= mLyricContent.size() - 2) { return mLyricContent.elementAt(index + 1).getTime() - mLyricContent.elementAt(index).getTime(); } else if (index == mLyricContent.size() - 1) { return INTERVAL_LAST_SENTENCE; // this is already the last sentence. } return INTERVAL_INVALID; } /* * get the current lyrics sentence's index according to the duration of the track being played. * * [in]millisecond: the current duration of the track being played. * * return value: the index of the current lyrics sentence. -1 if it fails the get the index. */ public int getCurrentIndex(int millisecond) { int low = 0; int high = mLyricContent.size() - 1; int ret = 0; if (INVALID_INDEX != mLastIndex) { if (millisecond > mLastMillisec) { ret = searchIndex(mLastIndex, high, millisecond); } else if (millisecond < mLastMillisec) { ret = searchIndex(low, mLastIndex, millisecond); } else { return mLastIndex; } } else { ret = searchIndex(low, high, millisecond); } mLastIndex = ret; mLastMillisec = millisecond; return ret; } /* * parse the real lyrics file (.lrc format) * * [in] filepathname: the absolute file path name of the .lrc file. */ private void parseLrc(String filepathname) throws LrcFileNotFoundException, LrcFileUnsupportedEncodingException, LrcFileIOException, LrcFileInvalidFormatException { String file = new String(filepathname); // dealing with different text file encodings. // (1) try reading BOM from text file FileInputStream lrcFileStream = null; try { lrcFileStream = new FileInputStream(new File(file)); } catch (FileNotFoundException e) { // if failed to load at the same folder then try /sdcard/Music/Lyrics // change the path to specified folder Scanner s = new Scanner(file); String fileName = s.findInLine(Pattern.compile("(?!.*/).*")); String anotherFile = new StringBuilder(mContext.getResources().getString(R.string.lrc_file_path)).append(fileName).toString(); try { lrcFileStream = new FileInputStream(new File(anotherFile)); file = anotherFile; } catch (FileNotFoundException ee) { throw new LrcFileNotFoundException( mContext.getResources().getString(R.string.lrc_file_not_found)); } } BufferedInputStream bin = null; String code = null; try { bin = new BufferedInputStream(lrcFileStream); int p = (bin.read() << 8) + bin.read(); // first 2 bytes int q = bin.read(); // 3rd byte // first check to see if it's Unicode Transition Format (UTF-8, UTF-16) switch (p) { // first check UTF-8 with BOM case 0xefbb: if (q == 0xbf) { code = "UTF-8"; // on windows, UTF-8 text files have a BOM: "EF BB BF"; } else { code = "UNKNOWN"; // however, on linux, there's no BOM for UTF-8 } break; // UTF-16 with BOM case 0xfffe: // little endian case 0xfeff: // big endian code = "UTF-16"; // the Scanner can recognize big endian or little endian break; default: code = "UNKNOWN"; break; } } catch (IOException e) { Log.d(LOG_TAG, "parseLrc : I/O error when reading .lrc file"); throw new LrcFileIOException( mContext.getResources().getString(R.string.lrc_file_io_error)); } // (2) if no BOM detected, we don't know if it's Unicode or ISO-8859-1 compatible encoding // try firstly to detect UTF-8 without BOM // by going through all the text file to see if there's one "character unit" that does not // match the UTF-8 encoding rule. if ("UNKNOWN".equals(code)) { try { lrcFileStream = new FileInputStream(new File(file)); } catch (FileNotFoundException e) { throw new LrcFileNotFoundException( mContext.getResources().getString(R.string.lrc_file_not_found)); } try { bin = new BufferedInputStream(lrcFileStream); byte[] value = new byte[3]; int result = 1; boolean isUTF8 = true; int byte1 = 0; int byte2 = 0; int byte3 = 0; while (result > 0) { result = bin.read(value, 0, 1); if (result <= 0) { break; } byte1 = value[0] & 0xff; if ((byte1 <= 0x7f) && (byte1 >= 0x01)) { // matches 1 byte encoding continue; } else { // need read one more byte result = bin.read(value, 1, 1); if (result <= 0) { break; } byte2 = value[1] & 0xff; if ((byte1 <= 0xdf) && (byte1 >= 0xc0) && (byte2 <= 0xbf) && (byte2 >= 0x80)) { // matches 2 bytes encoding continue; } else { // need read one more byte result = bin.read(value, 2, 1); if (result <= 0) { break; } byte3 = value[2] & 0xff; if ((byte1 <= 0xef) && (byte1 >= 0xe0) && (byte2 <= 0xbf) && (byte2 >= 0x80) && (byte3 <= 0xbf) && (byte3 >= 0x80)) { continue; } else { // don't match any of this it should not be UTF-8 isUTF8 = false; break; } } } } if (isUTF8) { code = "UTF-8"; // if detected as UTF-8 then change the "UNKNOWN" result } } catch (IOException e) { Log.d(LOG_TAG, "parseLrc : I/O error when reading .lrc file"); throw new LrcFileIOException( mContext.getResources().getString(R.string.lrc_file_io_error)); } } // if cannot be detected as Unicode series // then try with ISO-8859-1 compatible encoding according to device's default locale setting // create a scanner object according to the file name Scanner s = null; try { if ("UNKNOWN".equals(code)) { s = new Scanner(new File(file), LyricsLocale.defLocale2CharSet()); } else { s = new Scanner(new File(file), code); // UTF-8 or UTF-16 } } catch (FileNotFoundException e) { throw new LrcFileNotFoundException( mContext.getResources().getString(R.string.lrc_file_not_found)); } catch (IllegalArgumentException e) { // when the defLocale2CharSet returns null Log.d(LOG_TAG, "parseLrc : unsupported textual encoding"); throw new LrcFileUnsupportedEncodingException( mContext.getResources().getString(R.string.lrc_file_invalid_encoding)); } // (3) scanner success, clean up mLyricContent.clear(); // parse and add all possible lyrics sentences & information // the unrecognized lines in the file are omitted. while (s.hasNextLine()) { String next = s.nextLine(); // get a valid line if (next.length() < 1) { continue; // the empty line is omitted } // try to parse this line as a lyric sentence Integer[] integerArray = analyzeLrc(next); int len = integerArray.length; if (0 == len) { // no timeTag, thus it is a line of information or unrecognized line Scanner scn = new Scanner(next); // should match the .lrc file's "infoTag" format String info = scn.findInLine("\\[(al|ar|by|re|ti|ve):.*\\]"); if (null != info) { // if matches one of the info tags String tag = info.substring(1, 3); LyricSentence tmp = new LyricSentence(info.substring(4, info.length() - 1)); if (tag.equals("al") || tag.equals("AL")) { mAlbumName = tmp; } else if (tag.equals("ar") || tag.equals("AR")) { mArtistName = tmp; } else if (tag.equals("by") || tag.equals("BY")) { mLyricAuthor = tmp; } else if (tag.equals("re") || tag.equals("RE")) { mLyricBuilder = tmp; } else if (tag.equals("ti") || tag.equals("TI")) { mTrackTitle = tmp; } else if (tag.equals("ve") | tag.equals("VE")) { mLyricBuilderVer = tmp; } } else { // it's possible to be "offset" String offset = scn.findInLine("\\[offset:[\\+|-]{1}\\d+\\]"); if (null != offset) { // if matches offset tag mAdjustMilliSec = Integer.parseInt(offset.substring(9, offset.length() - 1)) * ( (offset.charAt(8) == '+') ? 1 : -1 ); } } } else { // has time tags, then it's a line of lyrics sentence for (int k = 0; k < len; k++) { int time = integerArray[k].intValue(); LyricSentence lrcSentence = new LyricSentence(resolveLrc(next), time + mAdjustMilliSec); mLyricContent.add(lrcSentence); } } } // sort the lyrics sentences as the sequence of time tags Collections.sort(mLyricContent); if (mLyricContent.isEmpty()) { throw new LrcFileInvalidFormatException( mContext.getResources().getString(R.string.lrc_file_invalid_format)); } } /* * analyze and retrieve the time tags from one line of the lyrics file; It's possible that * there are more than one time tags for each lyrics sentence. * * [in]origin: the original string of one line of the lyrics file * * return value: an Integer array, which will contain all the millisecond value of the timeTag. * if no timeTag is found in this line, the length of this array would be ZERO. */ private Integer[] analyzeLrc(String origin) { String str = new String(origin); Integer[] result = new Integer[0]; // first we assume that there's no timeTag List<Integer> list = new ArrayList<Integer>(); list.clear(); Scanner scn = new Scanner(str); String tmp = null; int tmpTimeValue = 0; // millisecond value // scan by regular expression, and calculate each millisecond value // there could be more than one timeTag for each lyric sentence. tmp = scn.findInLine("\\[\\d\\d:[0-5]\\d\\.\\d\\d\\]|\\[\\d\\d:[0-5]\\d\\]"); // [mm:ss.xx] or [mm:ss] while (null != tmp) { if (10 == tmp.length()) { // [mm:ss.xx] tmpTimeValue = Integer.parseInt(tmp.substring(1,3)) * 60000 // minutes + Integer.parseInt(tmp.substring(4,6)) * 1000 // second + Integer.parseInt(tmp.substring(7,9)) * 10; // 1/100 second } else if (7 == tmp.length()) { // [mm:ss] tmpTimeValue = Integer.parseInt(tmp.substring(1,3)) * 60000 // minutes + Integer.parseInt(tmp.substring(4,6)) * 1000; // second } list.add(Integer.valueOf(tmpTimeValue)); tmp = scn.findInLine("\\[\\d\\d:[0-5]\\d\\.\\d\\d\\]|\\[\\d\\d:[0-5]\\d\\]"); // next one } // convert the Integer list to actual Integer[] array // an suitable array that fits the length will be created. return list.toArray(result); } /* * analyze one line of the lyrics file and retrieve the lyric sentence * * [in]origin: the original string of one line of the lyrics file * * return value: a string, which is the pure part of one lyric sentence. */ private String resolveLrc(String origin) { String str = new String(origin); // remove all the valid time tags ([mm:ss.xx]) with an empty string str = str.replaceAll("\\[\\d\\d:[0-5]\\d\\.\\d\\d\\]|\\[\\d\\d:[0-5]\\d\\]", ""); // remove all extended time tags (<mm:ss.xx> format) str = str.replaceAll("\\<\\d\\d:[0-5]\\d\\.\\d\\d\\>|\\<\\d\\d:[0-5]\\d\\>", ""); str.trim(); return str; } /* * according to the duration of track being played, find the index of current sentence * * [in]low: the lower range of sentence index * [in]high: the higher range of sentence index * [in]millisecond: the current duration * * return value: the index representing the current lyrics sentence. -1 if it fails to search. */ private int searchIndex(int low, int high, int millisecond) { int mid; int cnt = mLyricContent.size() - 1; if (low < 0 || low > cnt || high < 0 || high > cnt || high < low) { throw new IllegalArgumentException(); } // should be the first sentence if (low == 0 && millisecond <= mLyricContent.elementAt(low).getTime() ) { return low; } // should be the last sentence if ((mLyricContent.size() - 1) == high && mLyricContent.elementAt(high).getTime() <= millisecond) { return high; } while (low < high) { mid = (high + low) / 2; if (mLyricContent.elementAt(mid).getTime() <= millisecond) { if (millisecond < mLyricContent.elementAt(mid + 1).getTime()) { return mid; } else { low = mid + 1; continue; } } else { if (mLyricContent.elementAt(mid - 1).getTime() <= millisecond) { return mid - 1; } else { high = mid - 1; continue; } } } return INVALID_INDEX; // -1 } } /* * ================================================================================================= */ /* * a single sentence of the real lyrics */ class LyricSentence implements Comparable<LyricSentence> { private final String text; // the actual lyrics sentence private final int milliSec; // in millisecond, the time interval from the beginning of track. // by default 0 milliseconds. /* * constructor */ public LyricSentence (String str) { this(str, 0); } public LyricSentence (String str, int min, int sec, int millisecond) { this(str, (min * 60 + sec) * 1000 + millisecond); } public LyricSentence (String str, int millisecond) { text = new String(str); milliSec = millisecond; } /* * to implement Comparable interface. */ public int compareTo(LyricSentence o) { if (this.milliSec < o.milliSec) { return -1; // earlier than the latter one } else if (this.milliSec > o.milliSec) { return 1; // the latter one is earlier } else { return 0; // equals } } public String getSentence() { return text; } public int getTime () { return milliSec; } } /* * ================================================================================================= */ class LyricsLocale { // add the locale value & default textual encoding name here if want to support more languages. private static final Locale zh_CN = new Locale("zh", "CN"); private static final Locale zh_TW = new Locale("zh", "TW"); private static final Locale en_US = new Locale("en", "US"); private static final String GBK = "GBK"; private static final String BIG5 = "Big5"; private static final String ISO_8859_1 = "ISO-8859-1"; // we use gbk encoding by default under English system setting // for CR ALPS00051690 private static final String DEF_ENCODING = "GBK"; /* * returns the character set to be used corresponding to device's default locale setting */ public static String defLocale2CharSet() { Locale loc = Locale.getDefault(); if (loc.equals(zh_CN)) { return GBK; } else if (loc.equals(zh_TW)) { return BIG5; } else if (loc.getLanguage().equals(en_US.getLanguage())) { return DEF_ENCODING; // default encoding for English language setting, CR ALPS00051690 } return null; // will cause IllegalArgumentException in LyricsBody -> parseLrc function } }